<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">

    <title>Cubemap UV — Alien.js</title>

    <link rel="preconnect" href="https://fonts.gstatic.com">
    <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto+Mono">
    <link rel="stylesheet" href="../assets/css/style.css">

    <script type="module">
        import { BasicMaterial, BufferGeometryLoader, BufferGeometryLoaderThread, Color, ColorManagement, Group, ImageBitmapLoaderThread, LinearSRGBColorSpace, Mesh, OrbitControls, PerspectiveCamera, Scene, TextureLoader, Vector2, WebGLRenderer, getFullscreenTriangle, ticker } from '../../build/alien.three.js';

        /*

        Instructions to export buffer geometry from Cinema 4D

        1) Select Model mode from the toolbar and select the object you would like to export.

        2) Load the UV grid texture from the assets directory into the Material Manager.

        3) Double-click on the UV grid Material and turn off Reflectance.

        4) Drag the UV grid Material to your object.

        5) Apply the "Set UVW from Projection" Material Tag, click the gear icon to change settings.

        If "Set UVW from Projection" is greyed-out you need to "Make Editable".

        6) We're going to export a local space object with the axis and coordinates at the centre of the object.

        If you need to centre the axis, from the Tools Axis menu, "Center Axis to".
        And set the world space coordinates of the object to 0 by setting the "Position" to 0 cm.

        7) The default size of objects in three.js is much smaller than the default in Cinema 4D.

        If you need to reduce the size, for example, a default sized cube in Cinema 4D is 200 cm.
        And units in three.js are normalized at 1 cm, so in this case you would set the "Size" to 2 cm.
        Alternatively, you can set the "Scale" to 0.01. Note that "Size" and "Scale" are different.
        You can find the "Size" of your object in the Coordinate Manager.

        8) From the Objects' File menu, export the selected object as an OBJ file, Default settings are fine.

        9) Open the three.js editor, create a new scene and import the OBJ file:
        https://threejs.org/editor/

        10) Click the plus sign (+) next to your object and select the first child object.

        11) Finally, re-export the object again with "Export Geometry".

        12) Outputs a .json with the required position, normal and uv attributes.

        Note you can only export one UV at a time.
        If you want to use uv1 you'll need to re-export with the other UV in slot 1.
        And then manually copy-paste the uv attribute into the .json as the uv1 attribute.

        13) Repeat for each of your objects.

        */

        class Cube extends Group {
            constructor() {
                super();
            }

            async initGeometry() {
                const { loadBufferGeometry } = WorldController;

                const geometry = await loadBufferGeometry('cube.json');

                // Use second set of UVs for cubemap row layout
                // https://docs.unrealengine.com/4.27/en-US/RenderingAndGraphics/Textures/Cubemaps/CreatingCubemaps/
                geometry.attributes.uv = geometry.attributes.uv1;

                this.geometry = geometry;
            }

            async initMaterial() {
                const { anisotropy, loadTexture } = WorldController;

                const map = await loadTexture('cubemap.jpg');
                map.anisotropy = anisotropy;

                this.material = new BasicMaterial({ map });
            }

            initMesh() {
                const mesh = new Mesh(this.geometry, this.material);
                this.add(mesh);
            }

            // Public methods

            ready = async () => {
                await Promise.all([
                    this.initGeometry(),
                    this.initMaterial()
                ]);

                this.initMesh();
            };
        }

        class SceneView extends Group {
            constructor() {
                super();

                this.visible = false;

                this.initViews();
            }

            initViews() {
                this.cube = new Cube();
                this.add(this.cube);
            }

            // Public methods

            animateIn = () => {
                this.visible = true;
            };

            ready = () => Promise.all([
                this.cube.ready()
            ]);
        }

        class RenderManager {
            static init(renderer, scene, camera) {
                this.renderer = renderer;
                this.scene = scene;
                this.camera = camera;
            }

            // Public methods

            static resize = (width, height, dpr) => {
                this.renderer.setPixelRatio(dpr);
                this.renderer.setSize(width, height);
            };

            static update = () => {
                this.renderer.render(this.scene, this.camera);
            };
        }

        class WorldController {
            static init() {
                this.initWorld();
                this.initLoaders();
                this.initControls();

                this.addListeners();
            }

            static initWorld() {
                this.renderer = new WebGLRenderer({
                    powerPreference: 'high-performance',
                    antialias: true
                });

                // Output canvas
                this.element = this.renderer.domElement;

                // Disable color management
                ColorManagement.enabled = false;
                this.renderer.outputColorSpace = LinearSRGBColorSpace;

                // 3D scene
                this.scene = new Scene();
                this.scene.background = new Color(0x060606);
                this.camera = new PerspectiveCamera(30);
                this.camera.near = 0.5;
                this.camera.far = 40;
                this.camera.position.z = 8;
                this.camera.lookAt(this.scene.position);

                // Global geometries
                this.screenTriangle = getFullscreenTriangle();

                // Global uniforms
                this.resolution = { value: new Vector2() };
                this.texelSize = { value: new Vector2() };
                this.aspect = { value: 1 };
                this.time = { value: 0 };
                this.frame = { value: 0 };

                // Global settings
                this.anisotropy = this.renderer.capabilities.getMaxAnisotropy();
            }

            static initLoaders() {
                this.textureLoader = new TextureLoader();
                this.textureLoader.setPath('../assets/textures/');

                this.bufferGeometryLoader = new BufferGeometryLoader();
                this.bufferGeometryLoader.setPath('../assets/geometry/');
            }

            static initControls() {
                this.controls = new OrbitControls(this.camera, this.renderer.domElement);
                this.controls.enableDamping = true;
                // this.controls.enableZoom = false;
            }

            static addListeners() {
                this.renderer.domElement.addEventListener('touchstart', this.onTouchStart);
            }

            // Event handlers

            static onTouchStart = e => {
                e.preventDefault();
            };

            // Public methods

            static resize = (width, height, dpr) => {
                this.camera.aspect = width / height;
                this.camera.updateProjectionMatrix();

                if (width < height) {
                    this.camera.position.z = 10;
                } else {
                    this.camera.position.z = 8;
                }

                width = Math.round(width * dpr);
                height = Math.round(height * dpr);

                this.resolution.value.set(width, height);
                this.texelSize.value.set(1 / width, 1 / height);
                this.aspect.value = width / height;
            };

            static update = (time, delta, frame) => {
                this.time.value = time;
                this.frame.value = frame;

                this.controls.update();
            };

            // Global handlers

            static getTexture = (path, callback) => this.textureLoader.load(path, callback);

            static loadTexture = path => this.textureLoader.loadAsync(path);

            static getBufferGeometry = (path, callback) => this.bufferGeometryLoader.load(path, callback);

            static loadBufferGeometry = path => this.bufferGeometryLoader.loadAsync(path);
        }

        class App {
            static async init() {
                this.initThread();
                this.initWorld();
                this.initViews();
                this.initControllers();

                this.addListeners();
                this.onResize();

                await this.view.ready();
                this.view.animateIn();
            }

            static initThread() {
                ImageBitmapLoaderThread.init();
                BufferGeometryLoaderThread.init();
            }

            static initWorld() {
                WorldController.init();
                document.body.appendChild(WorldController.element);
            }

            static initViews() {
                this.view = new SceneView();
                WorldController.scene.add(this.view);
            }

            static initControllers() {
                const { renderer, scene, camera } = WorldController;

                RenderManager.init(renderer, scene, camera);
            }

            static addListeners() {
                window.addEventListener('resize', this.onResize);
                ticker.add(this.onUpdate);
                ticker.start();
            }

            // Event handlers

            static onResize = () => {
                const width = document.documentElement.clientWidth;
                const height = document.documentElement.clientHeight;
                const dpr = window.devicePixelRatio;

                WorldController.resize(width, height, dpr);
                RenderManager.resize(width, height, dpr);
            };

            static onUpdate = (time, delta, frame) => {
                WorldController.update(time, delta, frame);
                RenderManager.update(time, delta, frame);
            };
        }

        App.init();
    </script>
</head>
<body>
</body>
</html>
